package org.interaction3d.assembly.convert.collada;

import org.interaction3d.assembly.convert.Material;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.interaction3d.assembly.convert.MeshUtil;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 *
 * @author micha
 */
public class ConverterDAE_1_4
{
    private static final int POSITION = 0, TEXTURE = 1, NORMAL = 2;

    public static void main(String... args) throws Exception
    {
        String desktopDir = System.getProperty("user.home") + File.separator + "Desktop" + File.separator;

        String file = desktopDir + "box.dae";
        //String file = desktopDir + "man.dae";
        //String file = desktopDir + "Skin_Morph.dae";
        new ConverterDAE_1_4(file, desktopDir);
    }

    private final String dstDir;
    //
    private final Document document;
    private final XPath xpath;


    private ConverterDAE_1_4(String srcFile, String dstDir) throws ParserConfigurationException, SAXException, IOException, XPathExpressionException
    {
        this.dstDir = dstDir;

            DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
            domFactory.setNamespaceAware(false);
            DocumentBuilder builder = domFactory.newDocumentBuilder();
            
            document = builder.parse(srcFile);

            xpath = XPathFactory.newInstance().newXPath();
            String version = (String) xpath.compile("COLLADA/@version").evaluate(document, XPathConstants.STRING);
            System.out.println("Version: " + version);
            if(!version.toLowerCase().startsWith("1.4"))
            {
                throw new IllegalArgumentException("Only Collada 1.4.x documents are supoprted by version is " + version);
            }            

            //new MaterialHandler();
            new MeshHandler();
            //new ModelHandler();
    }


    // <editor-fold defaultstate="collapsed" desc="Model">

    class ModelHandler
    {
        private final XPathExpression exprVisualScenes;
        private final XPathExpression exprNode;
        private final XPathExpression exprInstanceNode;
        
        ModelHandler() throws ParserConfigurationException, SAXException, IOException, XPathExpressionException
        {
            {
                exprVisualScenes =
                    xpath.compile("/COLLADA/library_visual_scenes/visual_scene[@id]");

                //TODO: find a better expression
                exprNode = xpath.compile("node");
                exprInstanceNode = xpath.compile("instance_node");
            }

            NodeList visualSceneNodes = (NodeList) exprVisualScenes.evaluate(document, XPathConstants.NODESET);

            int numScenes = visualSceneNodes.getLength();
            System.out.println("Scenes: " + numScenes);
            for (int i = 0; i < numScenes; i++)
            {
                Node visualSceneNode = visualSceneNodes.item(i);

                scene(visualSceneNode);

                System.out.println("-----------------------");
            }
        }

        void scene(Node visualSceneNode) throws XPathExpressionException
        {
            String modelId = visualSceneNode.getAttributes().getNamedItem("id").getTextContent();

            System.out.println("Model: " + modelId);

            NodeList sceneNodeNodes = (NodeList) exprNode.evaluate(visualSceneNode, XPathConstants.NODESET);
            int numNodes = sceneNodeNodes.getLength();
            for(int i=0; i<numNodes; i++)
            {
                Node sceneNodeNode = sceneNodeNodes.item(i);

                
            }
            


//            FileOutputStream out = null;
//            try
//            {
//                String dst = dstDir + modelId + ".model.xml";
//                out = new FileOutputStream(new File(dst));
//                //new ModelWriter(shading, constants, textures).writeMaterial(out);
//            }
//            catch (IOException ex)
//            {
//                Logger.getLogger(Collada_1_4.class.getName()).log(Level.SEVERE, null, ex);
//            }
//            finally
//            {
//                try
//                {
//                    out.close();
//                }
//                catch (IOException ex)
//                {
//                    Logger.getLogger(Collada_1_4.class.getName()).log(Level.SEVERE, null, ex);
//                }
//            }

        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="Material">

    class MaterialHandler
    {
        private final XPathExpression exprInstanceEffect;
        private final XPathExpression exprShading;
        private final XPathExpression exprFloat, exprColor, exprTexture;

        MaterialHandler() throws ParserConfigurationException, SAXException, IOException, XPathExpressionException
        {
            {
                exprInstanceEffect =
                    xpath.compile("/COLLADA/library_materials/material[@id]/instance_effect[starts-with(@url,'#')]");

                //TODO: find a better expression
                exprShading = xpath.compile("technique/blinn|technique/constant|technique/lambert|technique/phong");

                exprFloat = xpath.compile("*/float");
                exprColor = xpath.compile("*/color");
                exprTexture = xpath.compile("*/texture");
            }

            NodeList instanceEffectNodes = (NodeList) exprInstanceEffect.evaluate(document, XPathConstants.NODESET);

            int numMaterials = instanceEffectNodes.getLength();
            System.out.println("Materials: " + numMaterials);
            for (int i = 0; i < numMaterials; i++)
            {
                Node instanceEffectNode = instanceEffectNodes.item(i);
                material(instanceEffectNode);
                System.out.println("-----------------------");
            }
        }

        void material(Node instanceEffectNode) throws XPathExpressionException
        {
            String materialId = instanceEffectNode.getParentNode().getAttributes().getNamedItem("id").getTextContent();

            System.out.println("Material: " + materialId);

            String effectURL = instanceEffectNode.getAttributes().getNamedItem("url").getTextContent().substring(1);

            XPathExpression exprCommonProfile = xpath.compile("/COLLADA/library_effects/effect[@id='" + effectURL + "']/profile_COMMON");
            Node commonProfileNode = (Node) exprCommonProfile.evaluate(document, XPathConstants.NODE);
            if(exprCommonProfile == null)
            {
                System.out.println("Warning: profile_COMMON not found for matertial " + materialId);
                return;
            }
           
            Node shadingNode = (Node) exprShading.evaluate(commonProfileNode, XPathConstants.NODE);
            if(shadingNode == null)
            {
                System.out.println("Warning: only 'blinn', constant', 'lambert' and 'phong' shading is supported");
                return;
            }
            String shading = shadingNode.getNodeName();
            System.out.println("Shading: " + shading);

            Material material = new Material(shading);

            Map<String, double[]> constants = material.constants;
            Map<String, String>   textures  = material.textures;

            NodeList floatNodes = (NodeList) exprFloat.evaluate(shadingNode, XPathConstants.NODESET);
            {
                int count = floatNodes.getLength();
                for(int i=0; i<count; i++)
                {
                    Node floatNode = floatNodes.item(i);
                    String name = floatNode.getParentNode().getNodeName();
                    String value = floatNode.getTextContent();

                    System.out.println(name + ": " + value);
                    constants.put(name, parseDoubles(value, 1));
                    
                }
            }

            NodeList colorNodes = (NodeList) exprColor.evaluate(shadingNode, XPathConstants.NODESET);
            {
                int count = colorNodes.getLength();
                for(int i=0; i<count; i++)
                {
                    Node colorNode = colorNodes.item(i);
                    String name = colorNode.getParentNode().getNodeName();
                    String value = colorNode.getTextContent();

                    System.out.println(name + ": " + value);
                    constants.put(name, parseDoubles(value, 4));                    
                }
            }

            NodeList textureNodes = (NodeList) exprTexture.evaluate(shadingNode, XPathConstants.NODESET);
            {
                int count = textureNodes.getLength();
                for(int i=0; i<count; i++)
                {
                    Node textureNode = textureNodes.item(i);
                    String name = textureNode.getParentNode().getNodeName();

                    NamedNodeMap textureAttributes = textureNode.getAttributes();
                    String coord = textureAttributes.getNamedItem("texcoord").getTextContent();
                    String texture = textureAttributes.getNamedItem("texture").getTextContent();
                    if(false & !coord.equalsIgnoreCase("CHANNEL1"))
                    {
                        System.out.println("Warning: texcoord is not supported " + coord);
                    }


                    XPathExpression exprSampler2DSource = xpath.compile("newparam[@sid='" + texture + "']/sampler2D/source");
                    String sampler2DSource = (String) exprSampler2DSource.evaluate(commonProfileNode, XPathConstants.STRING);
                    if(sampler2DSource == null || sampler2DSource.isEmpty())
                    {
                        System.out.println("Warning: No sampler2D found");
                    }


                    XPathExpression exprSurface2DInitFrom = xpath.compile("newparam[@sid='" + sampler2DSource + "']/surface[@type='2D']/init_from");
                    String imgSource = (String) exprSurface2DInitFrom.evaluate(commonProfileNode, XPathConstants.STRING);
                    if(imgSource == null || imgSource.isEmpty())
                    {
                        System.out.println("Warning: No surface image found");
                    }
                   
                    XPathExpression exprImageInitFrom = xpath.compile("/COLLADA/library_images/image[@id='" + imgSource + "']/init_from");
                    String imageInitFrom = (String) exprImageInitFrom.evaluate(document, XPathConstants.STRING);
                    if(imageInitFrom == null || imageInitFrom.isEmpty())
                    {
                        System.out.println("Warning: No image found");
                    }

                    String value = imageInitFrom;

                    System.out.println(name + ": " + value);
                    textures.put(name, value);                    
                }
            }

            FileOutputStream out = null;
            try
            {
                String dst = dstDir + materialId + ".material.xml";
                out = new FileOutputStream(new File(dst));
                material.writeTo(out);
            }
            catch (IOException ex)
            {
                Logger.getLogger(ConverterDAE_1_4.class.getName()).log(Level.SEVERE, null, ex);
            }
            finally
            {
                try
                {
                    out.close();
                }
                catch (IOException ex)
                {
                    Logger.getLogger(ConverterDAE_1_4.class.getName()).log(Level.SEVERE, null, ex);
                }
            }

        }
    }

    static double[] parseDoubles(String text, int elements)
    {
        double[] array = new double[elements];
        parseDoubles(text, array);
        return array;
    }

    static void parseDoubles(String text, double[] elements)
    {
        Scanner scanner = new Scanner(text).useLocale(Locale.ENGLISH);

        for(int i=0; i<elements.length; i++)
        {
            elements[i] = scanner.nextDouble();
        }

        if(scanner.hasNext())
        {
            throw new RuntimeException("Mismatch");
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="Mesh">

    class MeshHandler
    {
        private final XPathExpression exprMesh, exprTriangles, exprInput, exprPrimitive, exprAccessor, exprFloatArray;
        private final XPathExpression exprNonTriangles;

        MeshHandler() throws ParserConfigurationException, SAXException, IOException, XPathExpressionException
        {            
            {
                exprMesh = xpath.compile("/COLLADA/library_geometries/geometry[@id]/mesh[descendant::triangles[child::p]]");
                exprTriangles = xpath.compile("triangles[child::p]");
                exprInput = xpath.compile("input[@semantic='VERTEX' | @semantic='NORMAL' | @semantic='TEXCOORD']");
                //exprInputCount = xpath.compile("count(input)");
                exprPrimitive = xpath.compile("p");
                exprAccessor =  xpath.compile("technique_common/accessor");
                exprFloatArray = xpath.compile("float_array");

                exprNonTriangles = xpath.compile("lines|linestrips|tristrips|trifans|polygons|polylist");
            }

            NodeList meshNodes = (NodeList) exprMesh.evaluate(document, XPathConstants.NODESET);

            int numMeshes = meshNodes.getLength();
            System.out.println("Meshes: " + numMeshes);
            for (int i = 0; i < numMeshes; i++)
            {
                Node meshNode = meshNodes.item(i);
                mesh(meshNode);
                System.out.println("-----------------------");
            }
        }

        Object mesh(Node meshNode) throws XPathExpressionException
        {
            String meshId = meshNode.getParentNode().getAttributes().getNamedItem("id").getTextContent();
            System.out.println("Mesh: " + meshId);

            // <editor-fold defaultstate="collapsed" desc="LOGGING: unsupported primitives">
            {
                NodeList notSupportedPrimitives = (NodeList) exprNonTriangles.evaluate(meshNode, XPathConstants.NODESET);
                int count;
                if ((count = notSupportedPrimitives.getLength()) > 0)
                {
                    System.out.println("\tWarning, Not supported primitives are ignored:");
                    for (int i = 0; i < count; i++)
                    {
                        if (i > 0)
                            System.out.print(", ");
                        else
                            System.out.print("\t");
                        Node node = notSupportedPrimitives.item(i);
                        System.out.print(node.getNodeName());
                    }
                    System.out.println();
                }
            }
            // </editor-fold>
            
            // <editor-fold defaultstate="collapsed" desc="Parse Triangles">
            PrimitiveGroup[] triangleGroups;
            {
                NodeList triagleNodes = (NodeList) exprTriangles.evaluate(meshNode, XPathConstants.NODESET);
                triangleGroups = new PrimitiveGroup[triagleNodes.getLength()];
                for (int i = 0; i < triangleGroups.length; i++)
                {
                    triangleGroups[i] = parseTriangleGroup(triagleNodes.item(i));
                }
            }
            // </editor-fold>

            // <editor-fold defaultstate="collapsed" desc="VERTEX -> POSITION">
            
            for (PrimitiveGroup triangleGroup : triangleGroups)
            {
                resolveVertexPosition(meshId, meshNode, triangleGroup.inputs);
            }
            // </editor-fold>
            
            // <editor-fold defaultstate="collapsed" desc=" Inputs ">
            
            Coordinates[] coordinates =
            {
                new Coordinates(),
                new Coordinates(),
                new Coordinates()
            };                        
            
            Map<String, int[][][]> primtiveGroups = new HashMap<String, int[][][]>();

            for (PrimitiveGroup triangleGroup : triangleGroups)
            {
                int numTriangles = triangleGroup.primitives.length;

                int[][][] primitives = new int[3][][];

                String[] semantics = { "POSITION", "TEXCOORD", "NORMAL" };

                for(int i=0; i<3; i++)
                {
                    Input input = Input.select(semantics[i], 0, triangleGroup.inputs);
                    if(input == null || input.hasExternalSource())
                    {
                        log(String.format("Warning Mesh (%s) has no or external %s source (%s)",
                            meshId, semantics[i], input.source));

                        continue;
                    }
                    
                    int offset;
                    {
                        Integer off = coordinates[i].offset(input.source);
                        if(off == null)
                        {
                            Node sourceNode = (Node) xpath.evaluate(sourceNode(input.source.substring(1)),
                                meshNode, XPathConstants.NODE);

                            if(sourceNode == null)
                            {
                                log(String.format("Warning Mesh (%s) has non local %s source (%s)",
                                    meshId, semantics[i], input.source));
                            }

                            double[][] data = parseSource(sourceNode);
                            offset = coordinates[i].offset(input.source, data);
                        }
                        else
                        {
                            offset = off;
                        }
                    }

                    primitives[i] = triangleGroup.primitives[input.offset];
                    if(offset != 0) for(int[] primitive : primitives[i])
                    {
                        for(int j=0; j<primitive.length; j++)
                        {
                            primitive[j] += offset;
                        }
                    }
                }
                
                for(int i=0; i<3; i++)
                {
                    if(primitives[i] != null)
                    {
                        continue;
                    }
                    primitives[i] = new int[numTriangles][3];
                    for(int [] primitive : primitives[i])
                    {
                        Arrays.fill(primitive, -1);
                    }
                }

                int[][][] other = primtiveGroups.get(triangleGroup.material);
                if(other == null)
                {
                    primtiveGroups.put(triangleGroup.material, primitives);
                }
                else
                {
                    primtiveGroups.put(triangleGroup.material,
                        combinePrimtiveGroups(other, primitives));
                }
            }
            // </editor-fold>

            MeshUtil meshUtil = new MeshUtil(coordinates[POSITION].elements(),
                                             coordinates[TEXTURE].elements(),
                                             coordinates[NORMAL].elements());


            for(Map.Entry<String, int[][][]> entry : primtiveGroups.entrySet())
            {
                String material = entry.getKey();
                int[][][] primitives = entry.getValue();
                meshUtil.primtives(material, "Triangles",
                    primitives[POSITION], primitives[TEXTURE], primitives[NORMAL]);
            }

            
            FileOutputStream out = null;
            try
            {
                String dst = dstDir + meshId + ".mesh.xml";
                out = new FileOutputStream(new File(dst));
                meshUtil.create().writeTo(out);
            }
            catch (IOException ex)
            {
                Logger.getLogger(ConverterDAE_1_4.class.getName()).log(Level.SEVERE, null, ex);
            }
            finally
            {
                try
                {
                    out.close();
                }
                catch (IOException ex)
                {
                    Logger.getLogger(ConverterDAE_1_4.class.getName()).log(Level.SEVERE, null, ex);
                }
            }

            return null;
        }


        private void resolveVertexPosition(String meshId, Node meshNode, Input[] inputs) throws XPathExpressionException
        {
            for(int i=0; i<inputs.length; i++)
            {
                Input input = inputs[i];

                if (!input.semantic.equals("VERTEX"))
                {
                    continue;
                }
                if(input.hasExternalSource())
                {
                    log(String.format("Warning Mesh (%s) has external source(%s)", meshId, input.source));
                    continue;
                }

                Input inputPosition;
                {
                    Input[] vtxInputs = parseInputs((NodeList)
                        xpath.evaluate(vertexPositionInputs(input.source.substring(1)),
                        meshNode, XPathConstants.NODESET));

                    inputPosition = Input.select("POSITION", 0, vtxInputs);
                }

                if(inputPosition != null)
                {
                    inputs[i] = new Input(input.offset, inputPosition.semantic, inputPosition.source, inputPosition.set);
                }
            }
        }

        // <editor-fold defaultstate="collapsed" desc=" Parsing ">

        PrimitiveGroup parseTriangleGroup(Node triangleNode) throws XPathExpressionException
        {
            int count;
            String material;
            {
                NamedNodeMap triangleListAttr = triangleNode.getAttributes();
                count  = Integer.parseInt(triangleListAttr.getNamedItem("count").getTextContent());

                Node materialNode = triangleListAttr.getNamedItem("material");
                material  = materialNode != null ? materialNode.getTextContent() : "";
            }
            Input[] inputs = parseInputs((NodeList) exprInput.evaluate(triangleNode, XPathConstants.NODESET));

            int groups = groups = Input.groups(inputs);

            String p = ((Node) exprPrimitive.evaluate(triangleNode, XPathConstants.NODE)).getTextContent();

            int[][][] triangles = Elements.triangles(p, count, groups);

            return new PrimitiveGroup(triangles, inputs, material, "Triangles");
        }

        double[][] parseSource(Node sourceNode) throws XPathExpressionException
        {
            //NamedNodeMap sourceAttrs = sourceNode.getAttributes();

            Node accessorNode = (Node) exprAccessor.evaluate(sourceNode, XPathConstants.NODE);

            if(accessorNode == null) throw new RuntimeException(sourceNode.getAttributes().getNamedItem("id").getTextContent());

            NamedNodeMap accessorAttrs = accessorNode.getAttributes();


            int count, offset = 0, stride = 1;
            {
                count = Integer.parseInt( accessorAttrs.getNamedItem("count").getTextContent() );

                Node offsetNode = accessorAttrs.getNamedItem("offset");
                if(offsetNode != null)
                {
                    offset = Integer.parseInt(offsetNode.getTextContent());
                }
                Node strideNode = accessorAttrs.getNamedItem("stride");
                if(strideNode != null)
                {
                    stride = Integer.parseInt(strideNode.getTextContent());
                }
            }

            {
                //TODO: check 'param'
            }

            String dataString = ((Node) exprFloatArray.evaluate(sourceNode, XPathConstants.NODE)).getTextContent();
            return Elements.coordinates( dataString, count, stride, offset );
        }

        // </editor-fold>

    }

    static int[][][] combinePrimtiveGroups(int[][][] a, int[][][] b)
    {
        if(a.length != b.length)
        {
            throw new IllegalArgumentException();
        }

        int[][][] combined = new int[a.length][][];
        for(int i=0; i<combined.length; i++)
        {
            combined[i] = new int[a[i].length + b[i].length][];
            for(int j=0; j>a[i].length; j++)
            {
                combined[i][j] = a[i][j];
            }
            for(int j=0; j>b[i].length; j++)
            {
                combined[i][a[i].length+j] = b[i][j];
            }
        }
        return combined;
    }

    static private String vertexPositionInputs(String id)
    {
        return "vertices[@id='" + id  + "']/input[@semantic='POSITION']";
    }

    static private String sourceNode(String id)
    {
        return "source[@id='" + id  + "']";
    }

    // <editor-fold defaultstate="collapsed" desc=" Parsing ">

    static Input[] parseInputs(NodeList inputNodes)
    {
        Input[] inputs = new Input[inputNodes.getLength()];
        for (int i = 0; i < inputs.length; i++)
        {
            inputs[i] = parseInput(inputNodes.item(i));
        }
        return inputs;
    }

    static Input parseInput(Node inputNode)
    {
        NamedNodeMap inputAttributes = inputNode.getAttributes();
        String semantic = inputAttributes.getNamedItem("semantic").getTextContent();
        String source = inputAttributes.getNamedItem("source").getTextContent();

        int set = 0, offset = 0;
        {
            Node offsetNode = inputAttributes.getNamedItem("offset");
            if(offsetNode != null) offset = Integer.parseInt(offsetNode.getTextContent());
        }
        {
            Node setNode = inputAttributes.getNamedItem("set");
            if(setNode != null) set = Integer.parseInt(setNode.getTextContent());
        }

        return new Input(offset, semantic, source, set);
    }


   

    // </editor-fold>

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="log">

    static private void log(String msg)
    {
        Logger.getLogger(ConverterDAE_1_4.class.getName()).log(Level.WARNING, msg);
    }

    static private void log(Throwable thrown)
    {
        log(null, thrown);
    }

    static private void log(String msg, Throwable thrown)
    {
        Logger.getLogger(ConverterDAE_1_4.class.getName()).log(Level.SEVERE, msg, thrown);
    }

    // </editor-fold>
}


